
local SandBox = commonlib.inherit(commonlib.ToolBase);

SandBox:Property("AllowFastMode", false, "IsAllowFastMode", "SetAllowFastMode");
SandBox:Property("StepMode", false, "IsStepMode", "SetStepMode");

local inject_map = {
	{"^(%s*function%A+[^%)]+%)%s*)$", "%1 checkyield();"},
	{"^(%s*local%s+function%W+[^%)]+%)%s*)$", "%1 checkyield();"},
	{"^(%s*for%s.*%s+do%s*)$", "%1 checkyield();"},
	{"^(%s*while%A.*%Ado%s*)$", "%1 checkyield();"},
	{"^(%s*repeat%s*)$", "%1 checkyield();"},
}

local inject_step_map = {
	{"^(%s*function%A+[^%)]+%)%s*)$", "%1 checkstep();"},
	{"^(%s*local%s+function%W+[^%)]+%)%s*)$", "%1 checkstep();"},
	{"^(%s*for%s.*%s+do%s*)$", "%1 checkstep();"},
	{"^(%s*while%A.*%Ado%s*)$", "%1 checkstep();"},
	{"^(%s*repeat%s*)$", "%1 checkstep();"},
	{"^(%s*end%s*)$", "%1 --"},
	{"^(%s*else%s*)$", "%1 --"},
	{"^(%s*if%(?.*then%s*)$", "%1 checkstep();"},
	{"^(%s*elseif%(?.*then%s*)$", "%1 checkstep();"},
	{"^(%s*%-%-.*)$", "%1 --"},
}

local function InjectLine(line)
	for _, v in ipairs(inject_map) do
		line = string.gsub(line, v[1], v[2]);
	end
	return line;
end

local function InjectCheckYieldToCode(code)
    if (not commonlib.IsString(code)) then return "" end
    local lines = {};
    local isInLongString = false;
    for line in string.gmatch(code, "([^\r\n]*)\r?\n?") do
        if(isInLongString) then
            lines[#lines + 1] = line;
            isInLongString = line:match("%]%]") == nil;
        else
            isInLongString = line:match("%[%[[^%]]*$") ~= nil;
            lines[#lines + 1] = InjectLine(line);
        end
    end
    code = table.concat(lines, "\n");
    return code;
end

local function InjectLineStep(lastLine)
	local line = lastLine
	for _, v in ipairs(inject_step_map) do
		line = string.gsub(line, v[1], v[2]);
	end
	if(line==lastLine and not line:match("^%s*$")) then
		line = "checkstep();" .. line;
	end
	return line;
end

local function InjectLineStepToCode(code)
    if (not commonlib.IsString(code)) then return "" end
    local lines = {};
    local isInLongString = false;
    for line in string.gmatch(code or "", "([^\r\n]*)\r?\n?") do
        if(isInLongString) then
            lines[#lines + 1] = line;
            isInLongString = line:match("%]%]") == nil;
        else
            isInLongString = line:match("%[%[[^%]]*$") ~= nil;
            lines[#lines + 1] = InjectLineStep(line);
        end
    end
    code = table.concat(lines, "\n");
    return code;
end

function SandBox:ctor()
    self.m_global = setmetatable({}, {__index = _G});
    self.m_log_text = "";

    self.m_global.__begin__ = function() self:Begin() end
    self.m_global.__end__ = function() self:End() end

    self.m_global.commonlib = commonlib;
    self.m_global.print = function(...) self:Print(...) end
    self.m_global.checkyield = function() commonlib.sleep() end
    self.m_global.checkstep = function() end
end

function SandBox:Print(...)
    print(...);

    if (self.m_log_text ~= "") then 
        self.m_log_text = self.m_log_text  .. "\n";
    end

    local args_size = select("#", ...);
    for index = 1, args_size do
        local arg = select(index, ...);
        self.m_log_text = self.m_log_text .. (index == 1 and "" or "    ") .. tostring(arg);
    end
end

function SandBox:CompileCode(code)
    if (not commonlib.IsString(code)) then return "" end
    if(not self:IsAllowFastMode()) then
        if(self:IsStepMode()) then
            code = InjectLineStepToCode(code)
        else
            code = InjectCheckYieldToCode(code)
        end
    end
    return _G.load(code, "sandbox", "bt", self.m_global);
end

function SandBox:Run(code)
    local func, errmsg = self:CompileCode(code);
    if (not func) then
        self.m_log_text = errmsg;
        self:Finish();
    else
        self.m_running_timer = commonlib.SetTimeout(0, function()
            self:Begin();
            func();
            self:End();
        end);
    end
end

function SandBox:Stop()
    if (not self.m_running_timer) then return end
    self.m_running_timer:Stop();
    self.m_running_timer = nil;
end

function SandBox:Begin()
    self.m_running = true;
end

function SandBox:End()
    self.m_running = false;
    self:Finish();
end

function SandBox:Finish()
    if (self.m_callback) then
        self.m_callback:CallVoid_String(self.m_log_text);
    end
end

function SandBox:GetLog()
    return self.m_log_text;
end

commonlib.RunSandBox = function(code, isAllowFastMode, isStepMode, callback)
    local sandbox = SandBox:new();
    sandbox.m_callback = callback;
    sandbox:SetAllowFastMode(isAllowFastMode);
    sandbox:SetStepMode(isStepMode);
    sandbox:Run(code);
    return sandbox;
end
